<?php
/**
* Scribble! This is a CMS of sorts.
*
* Copyright (c) 2009 James Gauld <james@jamesgauld.net>
* This file is part of Scribble.
* @license http://www.scribblecms.co.uk/license.txt
*
* @package Scribble
*/
use Buan\AutoLoader;
use Buan\Config;
use Buan\Core;
use Buan\GlobalEventDispatcher;
use Buan\IExtension;
use Buan\Model;
use Buan\ModelCriteria;
use Buan\ModelException;
use Buan\ModelManager;
use Buan\ModelRelation;
use Buan\SystemLog;
use Buan\UrlCommand;
use Buan\View;
class Scribble implements IExtension {

	/**
	* Holds all registered dashboard widgets.
	*
	* @var array
	*/
	private $dashboardWidgets = array();

	/**
	* Add some menus to the Scribble admin UI. The template you add will be
	* inserted directly into the <ul></ul> container of the admin menu, so your
	* templates should simply include the <li></li> nodes for each of your custom
	* menus.
	*
	* The menus can link anywhere you like, but in order to retain the overall
	* look of the admin UI, your controllers MUST extend ScribbleAdminController
	* and every action must call the ScribbleAdminController::init() method.
	*
	* If you want to hide the default admin menus generated by Scribble, and have
	* complete control over what menus appear, see the ::clearAdminMenuTemplates()
	* method.
	*
	* @param array|string Absolute path(s) to the admin menu template(s)
	* @return void
	*/
	public function addAdminMenuTemplate($path) {
		$paths = Config::get('ext.Scribble.adminMenuTemplates');
		Config::set('ext.Scribble.adminMenuTemplates', array_merge($paths, is_array($path) ? $path : array($path)));
	}

	/**
	* Adds some menu slots that will be made available to Scribble.
	*
	* @param string Comma-separated list of slot identifiers
	* @return void
	*/
	public function addMenuSlots($slots) {
		$current = Config::get('ext.Scribble.menuSlots');
		Config::set('ext.Scribble.menuSlots', array_merge($current, explode(",", $slots)));
	}

	/**
	* Clears all admin menu templates.
	*
	* @return void
	*/
	public function clearAdminMenuTemplates() {
		Config::set('ext.Scribble.adminMenuTemplates', array());
	}

	/**
	* Configure Scribble.
	*
	* This method performs the following taks:
	* - Adds all class paths to the autoloader
	* - Tell Buan where to find Scribble's controllers
	* - Set some [ext.*] configuration variables
	* - Strip the url suffix from [app.command.requested]
	* - Define some Model relationships
	* - Setup a class autoloader for all Swift* classes
	* - Auto-detect the current environment (ADMIN or FRONTEND)
	*
	* Some tasks you may want to perform immediately after calling this method:
	* - Change the environment depending on some custom rules
	*
	* The third parameter, $cmsUrl, allows you to change the URL prefix used to
	* access the Scribble admin UIs. So if you don't want to use "/scribble",
	* specifiy something else here (without leading/trailing slashes)
	*
	* @param string Directory to store Scribble data. Must be accessible via a URL
	* @param string The URL suffix that will be appended to all scribble-generated URLs
	* @param string The URL segment by which Scribble can be accessed
	* @return void
	*/
	public function configure($dataDir=NULL, $urlSuffix='.html', $cmsUrl=NULL) {

		// Add classpaths
		AutoLoader::addClassPath(dirname(__FILE__).'/lib');
		AutoLoader::addClassPath(dirname(__FILE__).'/lib/api-packages');
		AutoLoader::addClassPath(dirname(__FILE__).'/lib/dashboard-widgets');
		AutoLoader::addClassPath(dirname(__FILE__).'/models');
		AutoLoader::addClassPath(dirname(__FILE__).'/models/managers');
		include_once(dirname(__FILE__).'/lib/global-functions.php');

		// If using the non-default CMS URL, rewrite any incoming URLs to use this
		// custom prefix, and create a hook that will convert any newly generated
		// "/scribble..." URLs to using this custom prefix.
		if($cmsUrl!==NULL) {
			Config::set('app.command.requested', preg_replace("/^".preg_quote($cmsUrl, "/")."($|\/.*|".preg_quote($urlSuffix, "/").")$/", "scribble$1", Config::get('app.command.requested')));
			GlobalEventDispatcher::addEventListener('buan-pre-create-url', function($ev) use ($cmsUrl) {
				if($ev->data->controller=='scribble') {
					$ev->data->controller = $cmsUrl;
				}
			});
		}

		// Tell Buan where to look for this extension's controllers
		Config::set('ext.Scribble.dir.controllers', dirname(__FILE__).'/controllers');

		// Work out url root
		$dr = str_replace("\\", "/", isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : dirname(dirname(__FILE__)));
		$sr = str_replace("\\", "/", dirname(__FILE__));
		Config::set('ext.Scribble.urlRoot', str_replace($dr, "", $sr));

		// Setup some config variables
		Config::set('ext.Scribble.adminMenuTemplates', array());
		Config::set('ext.Scribble.adminTheme', Config::get('ext.Scribble.urlRoot').'/themes/default/theme.css');
		Config::set('ext.Scribble.appName', 'Scribble!');
		Config::set('ext.Scribble.dateFormat', 'd/m/Y');
		Config::set('ext.Scribble.dateTimeFormat', 'd/m/Y H:i');
		Config::set('ext.Scribble.dbTablePrefix', 'scribble_');
		Config::set('ext.Scribble.defaultTemplate', NULL);
		Config::set('ext.Scribble.htaccessSupport', getenv('SCRIBBLE_HTACCESS_OK')==1 ? TRUE : FALSE);
		Config::set('ext.Scribble.uploads.dir', Config::get('app.docRoot').'/cms-documents');
		Config::set('ext.Scribble.uploads.url', Config::get('app.urlRoot').'/cms-documents');
		Config::set('ext.Scribble.dir.resources', dirname(__FILE__).'/resources');
		Config::set('ext.Scribble.dir.views', dirname(__FILE__).'/views');
		Config::set('ext.Scribble.dir.data', $dataDir!==NULL ? $dataDir : dirname(__FILE__).'/app-data');
		Config::set('ext.Scribble.menuSlots', array());
		Config::set('ext.Scribble.urlSuffix', $urlSuffix);
		Config::set('ext.Scribble.version', array('major'=>0, 'minor'=>1, 'string'=>'0.1'));

		// Remove the urlSuffix from the [app.command.requested]
		Config::set('app.command.requested', preg_replace("/".preg_quote(Config::get('ext.Scribble.urlSuffix'), '/')."$/", "", Config::get('app.command.requested')));

		// Define Model relationships
		ModelRelation::define('Log(1):Log(M)');
		ModelRelation::define('Site(1):Page(M)');
		ModelRelation::define('Page(1):Page(M)');
		//ModelRelation::define('Page(1):PageContent(M)');
		ModelRelation::define('Page(1):PageComment(M)');
		ModelRelation::define('Page(1):PagePing(M)');
		//ModelRelation::define('Page(1):TagPage.page_id.tag(M):Tag(1)');
		//ModelRelation::define('Page.author_uid(M):User(1)', NULL, 'author');
		//ModelRelation::define('Page.publisher_uid(M):User(1)', NULL, 'publisher');
		ModelRelation::define('PageContent.author_id(M):User(1)');
		ModelRelation::define('User(1):UserRole(M):Role(1)');
		ModelRelation::define('Role(1):RolePrivileges(M)');

		ModelRelation::define('Page(1):PageVersion.pid(M)', NULL, 'master');
		ModelRelation::define('PageVersion(1):PageContent(M)');
		ModelRelation::define('PageVersion(1):TagPage.page_version_id.tag(M):Tag(1)');
		ModelRelation::define('PageVersion.author_id(M):User(1)', NULL, 'author');
		ModelRelation::define('PageVersion.releaser_id(M):User(1)', NULL, 'releaser');

		// Prepare a custom autoloader that will find class files for the
		// SwiftMailer library (ie. any classes that begin with "Swift")
		function swiftAutoLoader($className) {
			if(substr($className, 0, 5)!=='Swift') {
				return FALSE;
			}
			$classPath = dirname(__FILE__).'/lib/swift-mailer/lib/'.str_replace("_", "/", $className).".php";
			if(is_file($classPath)) {
				include($classPath);
				return TRUE;
			}
			else {
				die("Swift class not found: {$classPath}");
			}
		}
		spl_autoload_register('swiftAutoLoader');

		// Define some privileges
		ScribbleAuth::definePrivilege('General', 'scribble-admin', 'CMS Admin', 'Allow access to the CMS administration interfaces');
		ScribbleAuth::definePrivilege('General', 'scribble-upgrade', 'System Upgrade', 'Allow user to place the CMS in maintenance mode to perform upgrades');
		ScribbleAuth::definePrivilege('Content', 'site-author', 'Site Authoring', 'Modify sites', 'site-edit');
		ScribbleAuth::definePrivilege('Content', 'page-author', 'Page Authoring', 'Create and modify pages', 'page-edit,site-edit');
		ScribbleAuth::definePrivilege('Content', 'page-view-unpublished', 'Page Preview', 'View unpublished pages in frontend', 'page-edit,site-edit');
		ScribbleAuth::definePrivilege('Content', 'page-publish', 'Publisher', 'Manage page publication/expiration dates', 'page-edit,site-edit');
		ScribbleAuth::definePrivilege('Content', 'media-author', 'Media Control', 'Allow creation and removal of files in the media library');
		ScribbleAuth::definePrivilege('Content', 'media-folder-view', 'Media View', 'View content of media folders', 'media-folder-edit');
		ScribbleAuth::definePrivilege('User Access', 'user-admin', 'Privilege Admin', 'Create and modify users and roles');
		ScribbleAuth::definePrivilege('User Access', 'user-multi-login', 'Login As...', "Login under any other user's account");

		// Make an attempt at detecting whether we're in the ADMIN or FRONTEND
		// environments.
		if(preg_match("/^\/*scribble/i", Config::get('app.command.requested'))) {
			$environment = ScribbleEnvironment::ADMIN;
		}
		else {
			$environment = ScribbleEnvironment::FRONTEND;
		}
		ScribbleEnvironment::set($environment);

		// Initialise the Scribble session
		try {
			ScribbleSession::init();
		}
		catch(Exception $e) {
			die("Scribble start-up error: {$e->getMessage()}");
		}

		// Get the current site, or go through installation routine
		$site = SiteManager::getCurrentSite();
		if($site===NULL) {

			// Redirect to install, unless we're already requesting it
			if(strstr(Config::get('app.urlRoot').'/'.Config::get('app.command.requested'), UrlCommand::createUrl('scribble', 'install'))===FALSE) {
				ScribbleSession::logout();
				session_write_close();
				header("Location: ".UrlCommand::createUrl('scribble', 'install'), TRUE, 303);
				exit();
			}
		}

		// If the system is in "maintenance" mode then set an appropriate global
		// flag so various parts of the system can act appropriately.
		define('SCRIBBLE_MAINTENANCE_MODE', ScribbleRegistry::get('ext.Scribble.maintenanceMode')==1 ? TRUE : FALSE);
	}

	/**
	* Configuration routines that are only run whilst the user working in the
	* admin interfaces. The idea behind this is that we don't waste resources
	* configuring stuff that won't be executed in the front-end of the website.
	*
	* @return void
	*/
	private function configureAdmin() {

		// Handle maintenance mode
		if(SCRIBBLE_MAINTENANCE_MODE) {

			// Remind user they're in this mode
			SystemLog::add('This website is in maintenance mode', SystemLog::WARNING);

			// If the system needs upgrading from an older version, redirect to the
			// update interface
			if(Config::get('ext.Scribble.version.string')!=ScribbleRegistry::get('ext.Scribble.version.string')) {
				$upgradeUrl = ltrim(UrlCommand::createUrl('scribble', 'upgrade'), "/");
				if(!preg_match("/".preg_quote($upgradeUrl, "/")."/", Config::get('app.command.requested'))) {
					Config::set('app.command.requested', $upgradeUrl);
				}
			}
		}

		// Define the languages supported by the admin UIs
		Config::set('ext.Scribble.adminLanguages', array(
			array('code'=>'en', 'labelNative'=>'English'),
			array('code'=>'es', 'labelNative'=>'Espanol'),
			array('code'=>'en-sarcastic', 'labelNative'=>'Sarcastic English'),
		));

		// Add the default set of admin menus
		$this->addAdminMenuTemplate(Config::get('ext.Scribble.dir.views').'/core-admin-menus.tpl.php');

		// Add some dashboard widgets
		$wRecentActivity = new DWidgetRecentActivity();
		$this->registerDashboardWidget($wRecentActivity);

		// Add all required javascript and css sources to the view
		$ur = Config::get('ext.Scribble.urlRoot');
		View::getGlobalView()->addJavascripts(
			"{$ur}/js/jquery-1.4.2.min.js",
			"{$ur}/js/jquery-ui-1.8rc3.custom.min.js",
			"{$ur}/js/jquery.json-1.3.min.js",
			"{$ur}/js/jquery.autocomplete.min.js",
			"{$ur}/js/global-view/index.js"
		);
		View::getGlobalView()->addStylesheets(
			"{$ur}/css/jquery-themes/redmond/jquery-ui-1.8rc3.custom.css",
			Config::get('ext.Scribble.adminTheme')
		);

		// Add the css/js sources for the core admin UI widgets
		View::getGlobalView()->addJavascripts(
			"{$ur}/js/tinymce/tiny_mce.js",
			"{$ur}/js/admin-widgets/core.js",
			"{$ur}/js/admin-widgets/rte-configs.js",
			"{$ur}/js/admin-widgets/scribble-auth-widget.js",
			"{$ur}/js/admin-widgets/scribble-model-criteria-widget.js",
			"{$ur}/js/admin-widgets/scribble-schedule-widget.js"
		);
		View::getGlobalView()->addStylesheets(
			"{$ur}/js/admin-widgets/scribble-auth-widget.css"
		);
	}

	/**
	* Configure Scribble, and all registered plugins, for a specific environment.
	*
	* @return void
	*/
	public function configureEnvironment($environment) {

		// ADMIN
		if($environment===ScribbleEnvironment::ADMIN) {
			$this->configureAdmin();
		}

		// FRONTEND
		else {
			$this->configureFrontend();
		}

		// Configure all registered plugins
		$plugins = ScribblePluginManager::getAll();
		foreach($plugins as $plugin) {
			$plugin->configureEnvironment($environment);
		}

		// Trigger event
		$result = ScribbleEventManager::dispatchEvent(new ScribbleEvent('configure-environment-complete'));
	}

	/**
	* Configure things specific to the FRONTEND environment.
	*
	* @return void
	*/
	private function configureFrontend() {

		// Is there a matching Site for the requested domain?
		if(SiteManager::getCurrentSite()===NULL) {
			die("You have requested a site that is unknown to Scribble");
		}

		// If the system is in "maintenance" mode then don't continue as the page
		// delivery system (ie. ScribblePublicController) will be delivering the
		// "system under maintenance" message to frontend visitors.
		if(SCRIBBLE_MAINTENANCE_MODE) {
			return;
		}

		// Re-route RDF requests through to the "RdfMe" controller. This simply
		// finds "application/rdf+xml" in the Accept request header and redirects
		// if found.
		if(strpos($_SERVER['HTTP_ACCEPT'], 'application/rdf+xml')!==FALSE && !preg_match("/^rdf\-me/", Config::get('app.command.requested'))) {
			session_write_close();
			header("HTTP/1.1 303 See Other");
			header("Location: /rdf-me".$_SERVER['REQUEST_URI']);	// TODO: Improve this potential hazard
			exit();
		}
	}

	/**
	* Finds a suitable configuration for the requested site from those given in
	* $repo.
	*
	* $repo can be either a simple array, or a closure function which is executed
	* to do the job of this method.
	*
	* If $repo is an array it contains information about each site that this
	* installation of the application will support. Each element is indexed by the
	* site's domain/folder prefix. The format is as follows:
	*	array(
	*		'site-prefix'=>array(
	*			'bootstrap'=>'/absolute/path/to/bootstrap/script.php'
	*		),
	*		...
	*		'another-site'=>array(
	*			'aliasFor'=>'site-prefix',
	*			'httpRedirect'=>301
	*		)
	*	)
	*
	* Site prefixes are basically the domain, port and any subfolder, for example:
	*		www.mysite.com:8080/sub1/sub2
	*
	* If a prefix is actually an alias for another prefix, use the second of the
	* structures above, where 'aliasFor' contains the real site's prefix, and
	* 'httpRedirect' is the HTTP response code to use when redirecting (or leave
	* this empty/unset if you don't want to use redirects).
	*
	* If you wany any non-matches to default to a particular config, then provide
	* an entry in $repo indexed with the "__default__" prefix.
	*
	* If $repo is a closure function then it mjust take the domain:port prefix as
	* it's first argument and return the same result that this method would, ie.
	* an array containing a "bootstrap" index, or NULL:
	*		array('bootstrap'=>'/path/to/bootstrap')
	*
	* There's nothing to stop you from using the same bootstrap script for several
	* sites if you want to share resources, such as users and media assets, across
	* those sites, but just want different front-end Pages on each one.
	*
	* If a match is found, the configuration is returned and also store in the
	* following config paths:
	*		ext.Scribble.site.canonicalPrefix
	*		ext.Scribble.site.config
	*		ext.Scribble.site.prefix
	*
	* To get the Site model that represents the current site, use the
	* SiteManager::getCurrentSite() method.
	*
	* CURRENTLY ONLY SUPPORTS DOMAIN+PORT PREFIXES. PLAN TO INTRODUCE SUB-FOLDER
	* SUPPORT AT SOME POINT.
	*
	* @param array|Closure Site repository data (see structure above)
	* @return array|NULL Returns the matching site config from $repo, or NULL
	*/
	public function findSiteConfiguration($repo) {

		// Grab info about the requested URL
		$domain = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : NULL;
		$port = isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT']!='80' ? ":{$_SERVER['SERVER_PORT']}" : '';

		// If a closure has been specified as the repo, execute it passing the
		// domain:port prefix as the only argument
		if(is_object($repo) && $repo instanceof Closure) {
			return $repo("{$domain}{$port}");
		}

		// No match, use default
		if(!isset($repo["{$domain}{$port}"])) {
			if(!isset($repo['__default__'])) {
				return NULL;
			}
			$prefix = $canonicalPrefix = "{$domain}{$port}";
			$config = $repo['__default__'];
		}

		// Match: Alias prefix
		else if(isset($repo["{$domain}{$port}"]['aliasFor'])) {
			$prefix = "{$domain}{$port}";
			$alias = $repo[$prefix];
			$canonicalPrefix = $alias['aliasFor'];
			if(isset($alias['httpRedirect']) && in_array($alias['httpRedirect'], array(301, 302))) {
				header("HTTP/1.1 {$alias['httpRedirect']}");
				header("Location: http://{$alias['aliasFor']}");
				exit(0);
			}
			$config = $repo[$alias['aliasFor']];
		}

		// Match: Prefix
		else {
			$prefix = $canonicalPrefix = "{$domain}{$port}";
			$config = $repo[$prefix];
		}

		// Store in Buan config
		Config::set('ext.Scribble.site', array(
			'canonicalPrefix'=>$canonicalPrefix,
			'config'=>$config,
			'prefix'=>$prefix
		));
		return $config;
	}

	/**
	* Return all registered dashboard widgets.
	*
	* @return array
	*/
	public function getDashboardWidgets() {
		return $this->dashboardWidgets;
	}

	/**
	* Returns the upload folder ('dir') and url ('url') paths.
	*
	* @return StdClass
	*/
	public function getUploadPath() {
		$c = Config::get('ext.Scribble.uploads');
		$path = new StdClass();
		$path->dir = $c['dir'];
		$path->url = $c['url'];
		return $path;
	}

	/**
	* Run any required patching routines to makes the installed data compatible
	* with the new Scribble version, which is indicated by $to.
	*
	* Throws an Exception if patching fails at any point.
	*
	* @param string Version string (in the format "major.minor")
	* @param string Version string (in the format "major.minor")
	* @return void
	*/
	public function patchVersion($to, $from) {
	}

	/*
	* Registers a dashboard widget so it gets displayed in the dashboard UI.
	* The $widget instance must implement the IScribbleDashboardWidget interface.
	*
	* @param mixed Widget to add
	* @return void
	*/
	public function registerDashboardWidget($widget) {
		$this->dashboardWidgets[] = $widget;
	}

	/**
	* Sets the theme for styling the admin interfaces.
	*
	* @param string Absolute URL to the theme CSS file
	* @return void
	*/
	public function setAdminTheme($theme) {
		Config::set('ext.Scribble.adminTheme', $theme);
	}

	/**
	* Set the folder (and URL) that will be used for all media uploads.
	*
	* @param string Absolute path to storage folder
	* @param string Absolute URL to storage folder
	*/
	public function setUploadPath($folder, $url) {
		Config::set('ext.Scribble.uploads', array(
			'dir'=>$folder,
			'url'=>$url
		));
	}
}
?>
